Mastering TypeScript Decorators: A Comprehensive Guide to Enhancing Your Code

Why do we need a decorator?

If you’re new to TypeScript decorators, you may be wondering why they’re necessary. Before we delve into the answer, let’s take a look at a small code snippet.
notion image
a class and method without a typescript decorator
The getHomeAddress function violates the single responsibility principle because it performs multiple tasks that should be handled by separate functions. Specifically, the function is responsible for validating parameters, authorizing users, executing business logic, and handling errors. Although the primary purpose of the function is to call homeAddressApi and return the result, it also performs these additional tasks, which makes the function more complex and harder to maintain.
A better approach would be to delegate the validation, authorization, and error-handling logic to separate functions, allowing getHomeAddress to focus solely on the business logic of retrieving home addresses.
By using a TypeScript decorator, we can transform the original code into a more streamlined version that separates concerns and improves code maintainability.
notion image
a better method with typescript decorator
By using separate decorators to handle validation, authorization, and error handling, the getHomeAddress function is able to focus solely on the business logic of retrieving home addresses. This separation of concerns results in cleaner, more maintainable code that is easier to understand and modify. The getHomeAddress function is no longer cluttered with extraneous code, making it much simpler to work with.

Definition of Decorator:

  1. decorators are essentially functions or higher-order functions depending on their usage. It can be used like:
2. decorators are an object-oriented programming (OOP) feature and can only be used on class or class members, not on non-class functions. The following code is not valid:
getMyHomeAddress is just a standalone function. We cannot really decorate this function with the log decorator.
3. That being said, it is still possible to decorate a standalone function using the Lodash utility function flow. This allows for the creation of a chain of functions that can be applied to a standalone function, effectively "decorating" it with additional functionality. Here is an example:
notion image
a lodash flow example that shows how to decorate functions
In the previous example, the addDecorator and doubleDecorator functions added additional behavior to the add function without modifying the original function. This is similar to how decorators are used in classes, allowing for the addition of functionality to a class or class member without changing the original code.
Another example of using decorators is demonstrated in a Lodash flow function that includes a catchError decorator and a throwError function. The catchError decorator is able to catch errors that are thrown by the original function, allowing for more robust error handling.
notion image
a lodash flow example that shows how to use decorator to handle errors for functions
If you’re interested in experimenting with this concept, you can check out the provided Replit link: https://replit.com/@MINGWU1/function-decorators-with-lodash-flow?v=1

Additional Benefits with Decorators

Decorators offer additional benefits beyond simply adding functionality to a class or class member. One such benefit is the ability to enable powerful frameworks and patterns, such as Inversion of Control (IoC) and Dependency Injection (DI).

Inversion of Control (IoC) along with Dependency injection:

In the context of software development, “control” refers to manually creating instances of services or dependencies by calling new ServiceClass() within the target class. However, this can become problematic when the ServiceClass requires additional dependencies, which themselves require other dependencies. This can result in the creation of many instances, making the code difficult to manage.
Inversion of Control, or IoC, is a pattern that addresses this issue by delegating the responsibility of managing complicated dependency relationships to the framework. Instead of manually creating instances, the framework automatically injects the necessary dependencies into the target class when they are needed.
Dependency Injection, or DI, is a related pattern that involves injecting dependencies into a class rather than having the class create them itself. This helps to reduce coupling between classes and makes the code more modular and easier to maintain.
IoC and DI are heavily used in popular frameworks such as Angular, NestJS, .Net, and Spring Boot, allowing for more efficient and scalable application development.
  1. Let’s see a very simple angular LoggerService service and an app-root component:
notion image
A LoggerService class with an Injectable decorator
notion image
An app-root component that depends on the LoggerService class
In Angular, the @Injectable() decorator is used to register metadata for a class that can be injected as a dependency into other classes.
When a class is decorated with @Injectable(), it is recognized by the Angular Injector as a dependency. This means that if another class has a constructor that requires an instance of the @Injectable() class, the Angular Injector will automatically create an instance of the class and pass it to the constructor.
This allows Angular to act as a container that generates instances of services and injects them into the target class, without the need for manual instantiation. This is an example of Inversion of Control (IoC) and Dependency Injection (DI) at work in Angular.
2. Let’s see another simple example in nestjs
notion image
A nestjs controller with a Get and Post request handler
The @Controller@Get, and @Post decorators are used to add metadata to the UserController class and its methods. This metadata informs the NestJS framework which routes and HTTP methods should be associated with each method.
When a user makes a request to the web application, the framework checks the requested route and HTTP method against the metadata defined in the UserController class. If there is a match, the appropriate method is called and the result is returned to the user.
For example, if a user makes a GET request to the /user/list route, the framework checks the metadata defined by the @Get('/list') decorator and calls the userList() method. The userList() method then executes and returns the appropriate response to the user.
Similarly, if a user makes a POST request to the /user/add route, the framework checks the metadata defined by the @Post('/add') decorator and calls the addUser() method. The addUser() method then executes and returns the appropriate response to the user.
By using decorators to define the routes and HTTP methods for each method in the UserController class, we can keep our code clean and easy-to-read. This eliminates the need to manually define each route and method, saving us time and effort.

Aspect-oriented programming (AOP)

Do you still remember the very first code example called CustomerHomeAddress class? We used decorators to abstract away parameter validation and error handling in the CustomerHomeAddress class. However, issues like these are not limited to a single class or service, and can be present throughout an entire module or application. These issues are known as cross-cutting concerns.
The use of TypeScript decorators can be seen as a form of Aspect-Oriented Programming (AOP). By using decorators, you can separate these cross-cutting concerns from the rest of your application code, making them easier to manage and modify. This is similar to how AOP allows you to separate cross-cutting concerns using aspects, which can be applied to multiple parts of your application code.

Online React Examples with Decorators

Now, let’s take a look at some interesting examples in React and explore how decorators can be used to address issues such as validation, cache, and state management. While we won’t go into the specifics of how these solutions are implemented using decorators, you can experiment with them yourself to see how they work.
  1. Implementing validation with decorators in react
notion image
A react validation example with decorators
In the following example, we have a Course class that includes a title and price property. To ensure that the title is required and the price is a positive number, we use decorators to add metadata to the Course class.
notion image
A Course class with title and price. The title must be required and the price must be a positive number.
The Required and ValueType functions are used to add this metadata to the Course class. Additionally, a validator function reads this metadata and performs the necessary validation logic for the title and price properties. By using decorators in this way, we can ensure that our Course class adheres to the required validation rules without cluttering our code with extraneous logic.
2. Implementing cache with decorators in react
notion image
A react cache example with decorator
Here is the class with the decorator:
notion image
A Student class with cacheable decorator
This code defines a Student class that has a decorator called “cacheable”. When the “fetch address” button is clicked, it will trigger an API call that returns “Melbourne” after 2 seconds, and the result can be cached for 5 seconds. If the button is clicked again within 5 seconds, the cached result will be immediately returned. The “cacheable” decorator is responsible for caching the result for 5 seconds, and it can be used to add metadata to the Student class.
3. Implementing state management with decorators in react
notion image
A react state management example with decorators and proxy
Here is the decorated class:
notion image
A Store class with a reactive decorator
This code snippet represents a parent component (blue box) and a child component (pink box) that both display the message “Hello World”. Both the parent and child have a button that can modify their own message. It’s worth noting that the parent component did not pass any information to the child component.
Additionally, the code includes a “Store” class that has been decorated with a “reactive” decorator. This decorator makes the “message” property of the “Store” class reactive, meaning that any subscribers who read this value will be notified and updated simultaneously whenever the value changes.

Javascript Decorators

During our previous discussions, we focused extensively on TypeScript decorators. However, it’s important to note that TypeScript decorators are currently in stage 1 of implementation, while JavaScript decorators have progressed to stage 3. As a result, there are some notable differences between the two that we should keep in mind.
  1. The type of decorator is different:
notion image
notion image
The type of a class method typescript decorator: A Complete Guide to TypeScript Decorators | Disenchanted (mirone.me)
When comparing JavaScript decorators to TypeScript decorators, it’s clear that there are significant differences between the two.
2. Javascript decorator does not support decorating parameters and metadata.
3. New class auto-accessors
notion image
Javascript class accessor
The new accessor in JavaScript shares many similarities with the auto-property feature in C#.

Conclusion:

Through online React examples, we have learned about decorators and how they can be used to solve specific problems. It’s worth noting that while there are significant differences between JavaScript stage 3 decorators and TypeScript decorators, the core functionality of decorators remains the same. As a result, it’s important to be cautious when creating custom decorators, as they may need to be rewritten in the future.

References:


© ming 2021 - 2025